﻿namespace IronLua.Parsing

open System
open System.Collections.Generic
open System.Text.RegularExpressions
open IronLua

module internal Utilities =

    let hashset<'a> (seq : IEnumerable<'a>) = new HashSet<'a>(seq)
    let inhashset<'a> (h : HashSet<'a>) i = h.Contains(i)

    let symbols =    [| '('; ')'; '['; ']'; '{'; '}'; ','; '.'; ':'; ';'; 
                        '<'; '>'; '+'; '-'; '*'; '/'; '%'; '#'; '^';
                        '"'; '\''; '='; '~'; '|' |] |> hashset
    let wss =        [| ' '; '\t'; |]
    let newls =      [| '\n'; '\r'; |]
    let hexstart =   [| 'x'; 'X' |]
    let hexbinexp =  [| 'p' ; 'P' |]
    let numexp =     [| 'e'; 'E' |]
    let hexc =       [| 'a'; 'b'; 'c'; 'd'; 'e'; 'f'; 'A'; 'B'; 'C'; 'D'; 'E'; 'F' |]
    let keywords =   [| "do"; "goto"; "break"; "return"; "end"; "local"; 
                        "if"; "then"; "elseif"; "else"; "while"; "repeat"; 
                        "until"; "function"; "for"; "in"; |] |> hashset
    let ops =        [| "not"; "and"; "or"; "."; ":"; "<"; ">"; "=="; "<="; ">="; "!="; "~="
                        "+"; "-"; "*"; "/"; "+="; "-="; "*="; "/=";
                        "%"; "#"; "^"; "//"; |] |> hashset

    let getprevc i (str : string) = 
        if i > 0 then str.[i - 1] else char 0

    let getnextc i (str : string) = 
        let maxindex = str.Length - 1
        if i < maxindex then str.[i + 1] else char 0

    let cfindin chars c = 
        match chars |> List.tryFind (fun found -> found = c) with
        | Some(x) -> true
        | None ->    false

    let cfindinarr chars c = 
        match chars |> Array.tryFind (fun found -> found = c) with
        | Some(x) -> true
        | None ->    false

    let sfindin seps sym =
        match seps |> List.tryFind (fun s -> sym = s) with
        | Some(x) -> true
        | None ->    false

    let sfindinarr seps sym =
        match seps |> Array.tryFind (fun s -> sym = s) with
        | Some(x) -> true
        | None ->    false

    let rec slist = function
        | [] -> ""
        | hd :: [] -> sprintf "\"%s\"" hd
        | hd :: tl -> sprintf "\"%s\", \"%s\"" hd (slist tl)
                
    let rec firstOf = function
        | hd :: _ -> hd
        | _ -> failwith "Empty list."

    let rec lastOf = function
        | hd :: [] -> hd
        | hd :: tl -> lastOf tl
        | _ -> failwith "Empty list."

    // this works because Lua "should be" ASCII
    let (|Alpha|Num|Sym|Ws|Newline|Unknown|) c = 
        let ic = int c
        match ic with
        | uppercase when ic >= 65 && ic <= 90 ->    Alpha c
        | lowercase when ic >= 97 && ic <= 122 ->   Alpha c
        | number when ic >= 48 && ic <= 57 ->       Num (Char.GetNumericValue c)
        | newline when cfindinarr newls c ->        Newline c
        | whitespace when cfindinarr wss c ->       Ws c
        | sym when c |> inhashset symbols ->        Sym c
        | _ ->                                      Unknown

    let (|Keyword|Operator|Id|) s =
        match s with
        | k when s |> inhashset keywords -> Keyword
        | o when s |> inhashset ops ->      Operator
        | _ ->                              Id

    let inline isNull o = System.Object.ReferenceEquals(o, null)

    let splitLines (text:string) = 
        let text = text.Replace("\r\n", "\n").Replace("\r", "\n")
        System.Text.RegularExpressions.Regex.Split(text, "\n")

    let line no str = 
        let lines = splitLines str
        if no >= 0 && no < lines.Length then lines.[no]
        else ""

    let repStr chr len (endChr : char) = 
        if len < 1 then "" 
        else if len < 2 then endChr.ToString()
             else new String(chr, len - 1) + (string endChr)

    let errInfo (src : string) (pos : TokenPosition) = 
        let srcLine = src |> line (pos.Line - 1)
        let prevLine = src |> line (pos.Line - 2)
        let marker = repStr '-' pos.Column '^'
        if String.IsNullOrWhiteSpace(prevLine) then
            sprintf "%s\n%s" srcLine marker
        else
            sprintf "%s\n%s\n%s" prevLine srcLine marker

    let escapeLuaString str = str
    let findNewLineCol str = 
        let countNewLines = new Regex("\r\n|\n|\r", RegexOptions.Multiline)
        let matches = countNewLines.Matches(str)
        let count = matches.Count
        let col = 
            if count > 0 then 
               str.Length - matches.[count - 1].Index - 1
            else
                str.Length
        (count, col)
    let onStringMatch str (src : string) (value : string) (m : Match) (pos : TokenPosition) = 
        let str = m.Groups.["string"].Value
        let newLines, col = findNewLineCol value
        let index = pos.Index + value.Length
        let newPos = 
            if newLines > 0 then
                new TokenPosition(index, pos.Line + newLines, col, 0)
            else
                new TokenPosition(index, pos.Line, pos.Column + value.Length, 0)
        let pos = new TokenPosition(pos.Index, pos.Line, pos.Column, value.Length)
        Some(TokenMatch.Success(Token(TokenType.String, pos, escapeLuaString str), newPos))

    let inline tokenfailure (msg : string) (src : string) (pos : TokenPosition) = 
        failwith (sprintf "%s at line %i, column %i.\n%s" msg pos.Line pos.Column (errInfo src pos))

    let inline lexfailure (src : string) (pos : TokenPosition) =    
        failwith (sprintf "Unexpected symbol \"%c\" at line %i, column %i.\n%s" src.[pos.Index] pos.Line pos.Column (errInfo src pos))

    let inline sprintparsefailure (src : string) (msg : string) (pos : TokenPosition) = 
        sprintf "%s at line %i, column %i.\n%s" msg pos.Line pos.Column (errInfo src pos)  

    let inline parsefailure (src : string) (msg : string) (pos : TokenPosition) =
        let msg = sprintparsefailure src msg pos        
        LuaSyntaxException(msg) |> raise